package com.whale.kernel.service.hibernate;

import static javax.xml.stream.XMLStreamConstants.END_DOCUMENT;
import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;

import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.hibernate.type.StandardBasicTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.whale.kernel.domain.HtmlMenu;
import com.whale.kernel.exception.HtmlMenuException;
import com.whale.kernel.exception.ResourcesNotFoundException;
import com.whale.kernel.model.tree.HtmlMenuNode;
import com.whale.kernel.service.HtmlMenuService;
import com.whale.kernel.service.hibernate.transformer.Scalar;
import com.whale.kernel.service.hibernate.transformer.ScalarEntityMapTransformer;
import com.whale.kernel.utils.FileUtils;
import com.whale.kernel.utils.KeyableDataUtils;

public class HtmlMenuHibernateService extends AbstractHibernateService
		implements HtmlMenuService {

	private Logger logger = LoggerFactory
			.getLogger(HtmlMenuHibernateService.class);

	private final String menuNavigationConfig = "/resources/menu-navigation.xml";
	private final static String MENU_TAG = "menu";
	private final static String MENU_GROUP_TAG = "menu-group";
	private final static String PATH_ATTR = "path";
	private final static String NAME_ATTR = "name";
	private final static String MENU_GROUP_ID_ATTR = "id";

	private String parentMenusSql;
	private String childMenusSql;
	private ScalarEntityMapTransformer<HtmlMenu, HtmlMenuNode> htmlMenuNodeMapTrasformer;

	@Override
	protected void initialize() {
		parentMenusSql = FileUtils
				.readClassPathResource("com/whale/kernel/service/sql/nativesql/postgres/parentHtmlMenus.sql");
		childMenusSql = FileUtils
				.readClassPathResource("com/whale/kernel/service/sql/nativesql/postgres/childHtmlMenus.sql");
		htmlMenuNodeMapTrasformer = new ScalarEntityMapTransformer<HtmlMenu, HtmlMenuNode>() {

			public Class<HtmlMenu> getEntityClass() {
				return HtmlMenu.class;
			}

			public List<Scalar> getScalars() {
				List<Scalar> scalars = new ArrayList<Scalar>();
				scalars.add(new Scalar("stepnum", StandardBasicTypes.INTEGER));
				return scalars;
			}

			public Map<Long, HtmlMenuNode> transformResultsToMap(
					List<Object[]> list) {
				Map<Long, HtmlMenuNode> nodeMap = new HashMap<Long, HtmlMenuNode>();
				for (Object[] objects : list) {
					HtmlMenu menu = (HtmlMenu) objects[0];
					Integer level = (Integer) objects[1];
					// алгоритм для включения узлов с минимальным уровнем т.к. в
					// результате может быть информация от нескольких
					// родительских узлов одновременно и для узла будет
					// запоминаться его минимальный уровень вложенности. Это позволит
					// правильно сделать вывод о необходимости подгрузки узла с
					// сервера
					Long nodeId = menu.getId();
					if (nodeMap.containsKey(nodeId)) {
						HtmlMenuNode node = nodeMap.get(nodeId);
						if (node.getLocalNodeLevel() < level) {
							nodeMap.put(nodeId, node);
						}
					} else {
						HtmlMenuNode node = new HtmlMenuNode(menu, level);
						nodeMap.put(menu.getId(), node);
					}
				}
				return nodeMap;
			}

		};
	}

	public HtmlMenu findHtmlMenu(String path) {
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("path", path);
		return this.getUniqueQueryResult(HtmlMenu.class,
				"from HtmlMenu where path =:path", params);
	}

	public void reInitilizeHtmlMenu(ServletContext ctx) {
		InputStream istream = ctx.getResourceAsStream(menuNavigationConfig);
		if (istream == null) {
			throw new ResourcesNotFoundException(menuNavigationConfig);
		}
		try {
			reloadMenu(istream);
		} catch (XMLStreamException e) {
			logger.error(e.getMessage(), e);
			throw new HtmlMenuException(e.getMessage(), e);
		} finally {
			try {
				istream.close();
			} catch (IOException e) {
				throw new HtmlMenuException(e.getMessage(), e);
			}
		}
	}

	private void reloadMenu(InputStream istream) throws XMLStreamException {
		XMLInputFactory xmlif = createXMLInputFactory();
		XMLStreamReader xmlr = xmlif.createXMLStreamReader(istream);

		// clean all HtmlPage items
		executeUpdate("delete from HtmlPage");
		// clean all HtmlMenu items
		executeUpdate("delete from HtmlMenu");

		try {
			int event = xmlr.getEventType();
			while (xmlr.hasNext()) {
				switch (event) {
				case START_ELEMENT:
					if (xmlr.getLocalName().equals(MENU_GROUP_TAG)) {
						parseMenuGroup(xmlr);
					}
					break;
				case END_DOCUMENT:
					return;
				}
				event = xmlr.next();
			}
		} finally {
			xmlr.close();
		}

		return;
	}

	private void parseMenuGroup(XMLStreamReader xmlr) throws XMLStreamException {
		Map<String, String> attributes = new HashMap<String, String>();
		for (int i = 0; i < xmlr.getAttributeCount(); i++) {
			attributes.put(xmlr.getAttributeLocalName(i),
					xmlr.getAttributeValue(i));
		}

		String menuGroupId = attributes.get(MENU_GROUP_ID_ATTR);

		int menuOrder = 0;
		int event = xmlr.next();
		do {
			switch (event) {
			case START_ELEMENT:
				if (xmlr.getLocalName().equals(MENU_TAG)) {
					parseMenu(xmlr, null, menuGroupId, menuOrder);
					menuOrder++;
				}
				break;
			case END_ELEMENT:
				if (xmlr.getLocalName().equals(MENU_GROUP_TAG)) {
					return;
				}
				break;
			case END_DOCUMENT:
				return;
			default:
			}
			event = xmlr.next();
		} while (xmlr.hasNext());
	}

	private void parseMenu(XMLStreamReader xmlr, HtmlMenu parentMenu,
			String menuGroupId, int menuOrder) throws XMLStreamException {
		Map<String, String> attributes = new HashMap<String, String>();
		for (int i = 0; i < xmlr.getAttributeCount(); i++) {
			attributes.put(xmlr.getAttributeLocalName(i),
					xmlr.getAttributeValue(i));
		}
		HtmlMenu htmlMenu = new HtmlMenu();
		htmlMenu.setPath(attributes.get(PATH_ATTR));
		htmlMenu.setName(attributes.get(NAME_ATTR));
		htmlMenu.setParentMenu(parentMenu);
		htmlMenu.setMenuGroupId(menuGroupId);
		htmlMenu.setMenuOrder(menuOrder);
		hibernateTemplate.save(htmlMenu);

		int childMenuOrder = 0;
		int event = xmlr.next();
		do {
			switch (event) {
			case START_ELEMENT:
				if (xmlr.getLocalName().equals(MENU_TAG)) {
					parseMenu(xmlr, htmlMenu, null, childMenuOrder);
					childMenuOrder++;
				}
				break;
			case END_ELEMENT:
				if (xmlr.getLocalName().equals(MENU_TAG)) {
					return;
				}
				break;
			case END_DOCUMENT:
				return;
			default:
			}
			event = xmlr.next();
		} while (xmlr.hasNext());

	}

	public List<HtmlMenu> getTopLevelHtmlMenus(String menuGroupId) {
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("menuGroupId", menuGroupId);
		return this
				.getQueryResult(
						HtmlMenu.class,
						"from HtmlMenu where parentMenu is null and menuGroupId =:menuGroupId order by menuOrder",
						params);
	}

	public List<HtmlMenu> getAllTopLevelHtmlMenus() {
		Map<String, Object> params = new HashMap<String, Object>();
		return this.getQueryResult(HtmlMenu.class,
				"from HtmlMenu where parentMenu is null order by menuOrder",
				params);
	}

	public List<HtmlMenu> getSubHtmlMenus(String topLevelMenuPath) {
		HtmlMenu parentMenu = findHtmlMenu(topLevelMenuPath);
		if (parentMenu == null) {
			return new ArrayList<HtmlMenu>();
		}
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("parentMenu", parentMenu);
		return this
				.getQueryResult(
						HtmlMenu.class,
						"from HtmlMenu where parentMenu is not null and parentMenu =:parentMenu order by menuOrder",
						params);
	}

	public List<HtmlMenu> getParentMenus(Long menuId) {
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("rootMenuId", menuId);
		return this
				.getNativeQueryResult(HtmlMenu.class, parentMenusSql, params);
	}

	public Map<Long, HtmlMenuNode> getChildMenus(Long menuId, int maxLevel) {
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("menuId", menuId);
		params.put("maxLevel", maxLevel);

		String sql = MessageFormat.format(childMenusSql,
				"root.id = ( :menuId )");

		Map<Long, HtmlMenuNode> childs = getTreeMenus(sql, params);
		return childs;
	}

	public Map<Long, HtmlMenuNode> getTreeScopeMenus(Long menuId, int maxLevel) {
		List<HtmlMenu> parentMenus = getParentMenus(menuId);
		List<Long> parentMenuIds = KeyableDataUtils
				.transformToKeyList(parentMenus);
		// необходимо добавить и саму ветку
		parentMenuIds.add(menuId);

		Map<String, Object> params = new HashMap<String, Object>();
		params.put("rootMenuIds", parentMenuIds);
		params.put("maxLevel", maxLevel);

		String sql = MessageFormat.format(childMenusSql,
				"root.id in ( :rootMenuIds )");

		return getTreeMenus(sql, params);
	}

	public Map<Long, HtmlMenuNode> getTreeRootMenus(String menuGroupId,
			int maxLevel) {
		Map<String, Object> params = new HashMap<String, Object>();
		params.put("menuGroupId", menuGroupId);
		params.put("maxLevel", maxLevel);

		String sql = MessageFormat.format(childMenusSql,
				"root.parentMenu_id is null"
						+ " and root.menuGroupId =:menuGroupId");

		return getTreeMenus(sql, params);
	}

	private Map<Long, HtmlMenuNode> getTreeMenus(String sql,
			Map<String, Object> sqlParams) {
		return this.getNativeQueryResult(this.htmlMenuNodeMapTrasformer, sql,
				sqlParams);
	}

}
